CAN 简介

CAN 是 Controller Area Network 的缩写(以下称为 CAN),是 ISO 国际标准化的串行通信协议,CAN 的高性能和可靠性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被誉为自动化领域的计算机局域网。它的出现为分布式控制系统实现各节点之间实时、可靠的数据通信提供了强有力的技术支持。

CAN控制器通过组成总线的2根线(CAN-H和CAN-L)的电位差来确定总线的电平,在任一时刻,总线上有2种电平:显性电平和隐性电平。发送方通过使总线电平发生变化,将消息发送给接收方。

总线上执行逻辑上的线“与”时,显性电平的逻辑值为“0”,隐性电平为“1”。

与 IIC、SPI 等具有时钟信号的通信方式不同,CAN 通信并不是以时钟信号来进行同步的。它只具有 CAN_High 和 CAN_Low 两条信号线,共同构成一组差分信号线,所以CAN 是以差分信号的形式进行通信的。

CAN 的报文种类及结构

在 SPI 通信中,片选、时钟信号、数据输入及数据输出这四个信号都有单独的信号线。而 CAN 使用的是两条差分信号线,只能表达一个信号。简洁的物理层决定了 CAN 必然要配上一套更复杂的协议。如何用一个信号通道实现同样甚至更强大的功能呢?答案是对数据或操作命令进行打包。

CAN 协议特点:

1) 多主控制。

在总线空闲时,所有单元都可以发送消息(多主控制),而两个以上的单元同时开始发送消息时,根据标识符( Identifier 以下称为 ID)决定优先级。 ID 并不是表示发送的目的地址,而是表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息 ID 的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。

2) 系统的柔软性。

与总线相连的单元没有类似于“地址”的信息。因此在总线上增加单元时,连接在总线上的其它单元的软硬件及应用层都不需要改变。

3) 通信速度较快,通信距离远。

最高 1Mbps(距离小于 40M),最远可达 10KM(速率低于 5Kbps)。

4) 具有错误检测、错误通知和错误恢复功能。

所有单元都可以检测错误(错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能),正在发送消息的单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止(错误恢复功能)。

5) 故障封闭功能。

CAN 可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。

6) 连接节点多。

CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。正是因为 CAN 协议的这些特点,使得 CAN 特别适合工业过程监控设备的互连,因此,越来越受到工业界的重视,并已公认为最有前途的现场总线之一。

CAN 协议经过 ISO 标准化后有两个标准:ISO11898 标准和 ISO11519-2 标准。其中 ISO11898是针对通信速率为 125Kbps~1Mbps 的高速通信标准,而 ISO11519-2 是针对通信速率为 125Kbps以下的低速通信标准。

显性电平对应逻辑 0, CAN_H 和 CAN_L 之差为 2.5V 左右。
隐性电平对应逻辑 1, CAN_H 和 CAN_L 之差为 0V。

在总线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。而隐形电平则具有包容的意味,只有所有的单元都输出隐性电平,总线上才为隐性电平(显性电平比隐性电平更强)。另外,在 CAN 总线的起止端都有一个 120Ω的终端电阻,来做阻抗匹配,以减少回波反射。

CAN 协议是通过以下 5 种类型的帧进行的:

  • 数据帧
  • 遥控帧
  • 错误帧
  • 过载帧
  • 帧间隔

数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有 11 个位的标识符ID,扩展格式有 29 个位的 ID。

帧类型 帧用途
数据帧 用于发送单元向接收单元传送数据的帧
遥控帧 用于接收单元向具有相同 ID 的发送单元请求数据的帧
错误帧 用于当检测出错误时向其它单元通知错误的帧
过载帧 用于接收单元通知其尚未做好接收准备的帧
间隔帧 用于将数据帧及遥控帧与前面的帧分离开来的帧

数据帧一般由 7 个段构成,即:
( 1) 帧起始。表示数据帧开始的段。
( 2) 仲裁段。表示该帧优先级的段。
( 3) 控制段。表示数据的字节数及保留位的段。
( 4) 数据段。数据的内容,一帧可发送 0~8 个字节的数据。
( 5) CRC 段。检查帧的传输错误的段。
( 6) ACK 段。表示确认正常接收的段
( 7) 帧结束。表示数据帧结束的段。

STM32 自带的是 bxCAN,即基本扩展 CAN。它支持 CAN 协议 2.0A 和 2.0B。它的设
计目标是,以最小的 CPU 负荷来高效处理大量收到的报文。它也支持报文发送的优先级要求(优
先级特性可软件配置)。对于安全紧要的应用, bxCAN 提供所有支持时间触发通信模式所需的
硬件功能。

STM32 的 bxCAN 的主要特点有:

  • 支持 CAN 协议 2.0A 和 2.0B 主动模式
  • 波特率最高达 1Mbps
  • 支持时间触发通信
  • 具有 3 个发送邮箱
  • 具有 3 级深度的 2 个接收 FIFO
  • 可变的过滤器组(最多 28 个)

同步
由于 CAN 没有时钟信号线,而且它的报文中并没有包含用于同步的标志,所以要使
位同步的方式来确保通信时序,以及对总线的电平进行正确采样。

STM32 CAN 控制器

STM32 自带的是 bxCAN,即基本扩展 CAN。它支持 CAN 协议 2.0A 和 2.0B。它的设
计目标是,以最小的 CPU 负荷来高效处理大量收到的报文。它也支持报文发送的优先级要求(优
先级特性可软件配置)。对于安全紧要的应用, bxCAN 提供所有支持时间触发通信模式所需的
硬件功能。
STM32 的 bxCAN 的主要特点有:

  • 支持 CAN 协议 2.0A 和 2.0B 主动模式
  • 波特率最高达 1Mbps
  • 支持时间触发通信
  • 具有 3 个发送邮箱
  • 具有 3 级深度的 2 个接收 FIFO
  • 可变的过滤器组(最多 28 个)

STM32 的标识符过滤是一个比较复杂的东东,它的存在减少了 CPU 处理 CAN 通信的开销。STM32 的过滤器组最多有 28 个(互联型),但是 STM32F103ZET6 只有 14 个(增强型),每个滤波器组 x 由2个 32 位寄存器, CAN_FxR1 和 CAN_FxR2 组成。

STM32 每个过滤器组的位宽都可以独立配置,以满足应用程序的不同需求。根据位宽的不
同,每个过滤器组可提供:

  • 1 个 32 位过滤器,包括: STDID[10:0]、 EXTID[17:0]、 IDE 和 RTR 位
  • 2 个 16 位过滤器,包括: STDID[10:0]、 IDE、 RTR 和 EXTID[17:15]位
    此外过滤器可配置为,屏蔽位模式和标识符列表模式

Tx Mailboxes(发送邮箱)
STM32 的 CAN 中共有 3 个发送邮箱供软件来发送报文。发送调度器根据优先级决定
哪个邮箱的报文先被发送。

Accepttance Filters( 接收过滤器 )
STM32 的 CAN 中共有 14 个位宽可变/可配置的标识符过滤器组,软件通过对它们编
程,从而在 CAN 收到的报文中选择它需要的报文,而把其它报文丢弃掉。

Receive FIFO( 接收 FIFO )
STM32 的 CAN 中共有 2 个接收 FIFO,每个 FIFO 都可以存放 3 个完整的报文。它们
完全由硬件来管理。

CAN 的工作就是围绕这三部分展开的

STM32 CAN 位时序

CAN 波特率的计算公式,我们只需要知道 BS1 和 BS2 的设置,以及 APB1
的时钟频率(一般为 36Mhz),就可以方便的计算出波特率。比如设置 TS1=6、 TS2=7 和 BRP=4,
在 APB1 频率为 36Mhz 的条件下,即可得到 CAN 通信的波特率=36000/[(7+8+1) * 5]=450Kbps。

CAN 的主控制寄存器 CAN_MCR

CAN 的初始化配置步骤

CAN 相关的固件库函数和定义分布在文件 stm32f10x_can.c 和头文件 stm32f10x_can.h 文件中。

1)配置相关引脚的复用功能,使能 CAN 时钟

2)设置 CAN 工作模式及波特率等。
这一步通过先设置 CAN_MCR 寄存器的 INRQ 位,让 CAN 进入初始化模式,然后设置
CAN_MCR 的其他相关控制位。再通过 CAN_BTR 设置波特率和工作模式(正常模式/环回模式)
等信息。 最后设置 INRQ 为 0,退出初始化模式。

库函数中,提供了函数 CAN_Init()用来初始化 CAN 的工作模式以及波特率, CAN_Init()函数体中,在初始化之前,会设置 CAN_MCR 寄存器的 INRQ 为 1 让其进入初始化模式,然后初始化 CAN_MCR 寄存器和 CRN_BTR 寄存器之后,会设置 CAN_MCR 寄存器的 INRQ 为 0让其退出初始化模式。所以我们在调用这个函数的前后不需要再进行初始化模式设置。

初始化实例为:

1
2
3
4
5
6
7
8
9
10
11
12
13
CAN_InitStructure.CAN_TTCM=DISABLE; //非时间触发通信模式
CAN_InitStructure.CAN_ABOM=DISABLE; //软件自动离线管理
CAN_InitStructure.CAN_AWUM=DISABLE; //睡眠模式通过软件唤醒
CAN_InitStructure.CAN_NART=ENABLE; //禁止报文自动传送
CAN_InitStructure.CAN_RFLM=DISABLE; //报文不锁定,新的覆盖旧的
CAN_InitStructure.CAN_TXFP=DISABLE; //优先级由报文标识符决定
CAN_InitStructure.CAN_Mode= CAN_Mode_LoopBack; //模式设置: 1,回环模式;
//设置波特率
CAN_InitStructure.CAN_SJW=CAN_SJW_1tq;//重新同步跳跃宽度为个时间单位
CAN_InitStructure.CAN_BS1=CAN_BS1_8tq; //时间段 1 占用 8 个时间单位
CAN_InitStructure.CAN_BS2=CAN_BS2_7tq;//时间段 2 占用 7 个时间单位
CAN_InitStructure.CAN_Prescaler=5; //分频系数(Fdiv)
CAN_Init(CAN1, &CAN_InitStructure); // 初始化 CAN1

3)设置滤波器。
先设置 CAN_FMR的 FINIT 位,让过滤器组工作在初始化模式下,然后设置滤波器组 0 的工作模式以及标识符 ID
和屏蔽位。最后激活滤波器,并退出滤波器初始化模式。

过滤器初始化参考实例代码:

1
2
3
4
5
6
7
8
9
10
CAN_FilterInitStructure.CAN_FilterNumber=0; //过滤器 0
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //32 位
CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000;////32 位 ID
CAN_FilterInitStructure.CAN_FilterIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000;//32 位 MASK
CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;// FIFO0
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器 0
CAN_FilterInit(&CAN_FilterInitStructure);//滤波器初始化

至此, CAN 就可以开始正常工作了。如果用到中断,就还需要进行中断相关的配置,

4)发送接受消息

在初始化 CAN 相关参数以及过滤器之后,接下来就是发送和接收消息了。 库函数中提供
了发送和接受消息的函数。

发送消息的函数是:

1
uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);

第一个参数是 CAN 标号,我们使用 CAN1。第二个参数是相关消息结构
体 CanTxMsg 指针类型, CanTxMsg 结构体的成员变量用来设置标准标识符,扩展标示符,消
息类型和消息帧长度等信息。

接受消息的函数是:

1
void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage;

前面两个参数也比较好理解, CAN 标号和 FIFO 号。 第二个参数 RxMessage 是用来存放接受到
的消息信息。

结构体 CanRxMsg 和结构体 CanTxMsg 比较接近,分别用来定义发送消息和描述接受消息,

5) CAN 状态获取
对于 CAN 发送消息的状态,挂起消息数目等等之类的传输状态信息,库函数提供了一些列的函数,包括 CAN_TransmitStatus()函数, CAN_MessagePending()函数, CAN_GetFlagStatus()函数等等,大家可以根据需要来调用。

CAN 的中断由发送中断、接收 FIFO 中断和错误中断构成。发送中断由三个发送邮箱
任意一个为空的事件构成。接收 FIFO 中断分为 FIFO0 和 FIFO1 的中断,接收 FIFO 收到
新的报文或报文溢出的事件可以引起中断。本实验中使用的 RX0 中断通道即为 FIFO0 中
断通道,当 FIFO0 收到新报文时,引起中断,我们就在相应的中断服务函数读取这个新报

打包报文

配置好 CAN 接口后,我们就可以复用它来发送数据了。利用 CAN 发送数据,要先把数据打包成完整的 CAN 报文格式。

void CAN_SetMsg(void)
{
//TxMessage.StdId=0x00;
TxMessage.ExtId=0x1314; //使用的扩展 ID
TxMessage.IDE=CAN_ID_EXT; //扩展模式
TxMessage.RTR=CAN_RTR_DATA; //发送的是数据
TxMessage.DLC=2; //数据长度为 2 字节
TxMessage.Data[0]=0xAB;
TxMessage.Data[1]=0xCD;
}

函数使用的结构体变量 TxMessage 在 main 文件以全局变量的形式定义,结构体变量 TxMessage 和 RxMessage.

1
2
CanTxMsg TxMessage; // 发送缓冲区
CanRxMsg RxMessage; // 接收缓冲区

TxMessage 的类型为 CanTxMsg,而接收报文时,我们使用 CanRxMsg 类型。它们都是由库文件定义的结构体类型。

报文打包函数

1
2
3
4
5
6
7
8
9
10
void CAN_SetMsg(void)
{
//TxMessage.StdId=0x00;
TxMessage.ExtId=0x1314; //使用的扩展 ID
TxMessage.IDE=CAN_ID_EXT; //扩展模式
TxMessage.RTR=CAN_RTR_DATA; //发送的是数据
TxMessage.DLC=2; //数据长度为 2 字节
TxMessage.Data[0]=0xDC;
TxMessage.Data[1]=0xBA;
}

接收报文结构体 CanRxMsg

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
uint32_t StdId; /* 接收报文的标准 ID */
uint32_t ExtId; /* 接收报文的扩展 ID */
uint8_t IDE; /* 报文的 IDE 位 */
uint8_t RTR; /* 报文的 RTR 位 */
uint8_t DLC; /* 报文的 DLC 段 */
uint8_t Data[8]; /* 报文的数据段 */
uint8_t FMI; /* 过滤器匹配序号 */

} CanRxMsg;

接收报文、编写中断服务函数

从机中断服务函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void USB_LP_CAN1_RX0_IRQHandler(void)
{
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

/* 比较是否是发送的数据和 ID */

if ((RxMessage.ExtId==0x1314) && (RxMessage.IDE==CAN_ID_EXT)&& (RxMessage.DLC==2) \
&&((RxMessage.Data[1]|RxMessage.Data[0]<<8)==0xABCD))
{
flag = 0; //接收成功
}
else
{
flag = 0xff; //接收失败
}
}

在中断服务函数中,我们调用了库函数 CAN_Receive() 把 FIFO0 的报文读取到 main
文件的 CanRxMsg 类型全局变量 RxMessage 中。

使用 CAN_Receive() 函数接收了报文后,我们使用 if 语句判断接收到的报文的 ID 信息、IDE 位、DLC 位及数据段是否等于 0xABCD,若接收到的报文与我们主机预定发送的报文一样,则把 fl ag 位置“0”,退出中断服务函数。